查看原文
其他

老司机避坑指南:如何快速搞定微服务架构?

陈峻编译 技术琐话 2019-04-21

如今,微服务架构已经成为了现代应用开发的首选。虽然它能够解决大部分的程序问题,但是它并非一颗百试不爽的“银弹”。


在采用这种架构之前,我们应当事先了解可能出现的各种问题及其共性,预先为这些问题准备好可重用的解决方案。


那么,在开始深入讨论微服务的不同设计模式之前,让我们先了解一下微服务架构的一些构建原则:

  • 可扩展性

  • 可用性

  • 弹性

  • 独立、自主性

  • 去中心化治理

  • 故障隔离

  • 自动调配

  • 通过 DevOps 实现持续交付


在遵循上述各条原则的同时,我们难免会碰到一些挑战。下面我们来具体讨论可能出现的各种问题、及其解决方案。


分解模式


按照业务功能分解


问题:微服务是有关松散耦合的服务,它采用的是单一职责原则。虽然我们在逻辑原理上都知道要将单个应用分成多个小块,但是在实际操作中,我们又该如何将某个应用程序成功分解成若干个小的服务呢?


解决方案:有一种策略是按照业务功能进行分解。此处的业务功能是指能够产生价值的某种业务的最小单位。那么一组给定业务的功能划分则取决于企业本身的类型。


例如,一家保险公司的功能通常会包括:销售、营销、承保、理赔处理、结算、合规等方面。每一个业务功能都可以被看作是一种面向业务、而非技术的服务。


按照子域分解


问题:按照业务功能对应用程序进行分解只是一个良好的开端,之后您可能会碰那些不易分解的所谓“神类”(God Classes)。这些类往往会涉及到多种服务。


例如,订单类就会被订单管理、订单接受、订单交付等服务所使用到,那么我们又该如何分解呢?


解决方案:对于“神类”的问题,DDD(Domain Driven Design,领域驱动设计)能够派上用场。


它使用子域(Subdomain)和边界上下文(Bounded Context)的概念来着手解决。


DDD 会将企业的整个域模型进行分解,并创建出多个子域。每个子域将拥有一个模型,而该模型的范围则被称为边界上下文。那么每个微服务就会围绕着边界上下文被开发出来。


注意:识别子域并不是一件容易的事,我们需要通过分析业务与组织架构,识别不同的专业领域,来对企业加强了解。


刀砍模式(Strangler Pattern)


问题:前面我们讨论的设计模式一般适用于针对那些“白手起家”的 Greenfield 应用进行分解。


但是我们真实接触到的、约占 80% 的是 Brownfield 应用,即:一些大型的、单体应用(Monolithic Application)。


由于它们已经被投入使用、且正在运行,如果我们简单按照上述方式,同时对它们进行小块服务的分解,将会是一项艰巨的任务。


解决方案:此时,刀砍模式(Strangler Pattern)就能派上用场了。我们可以把扼杀模式想象为用刀砍去缠在树上的藤蔓。


该方案适用于那些反复进行调用的 Web 应用程序。对于每一个 URI(统一资源标识符)的调用来说,单个服务可以被分解为不同的域和单独的子服务。其设计思想是一次仅处理一个域。


这样,我们就可以在同一个 URI 空间内并行地创建两套独立的应用程序。最终,在新的应用重构完成后,我们就能“刀砍”或替换掉原来的应用程序,直到最后我们可以完全关闭掉原来的单体应用。

集成模式


API 网关模式


问题:当一个应用程序被分解成多个小的微服务时,我们需要关注如下方面。


具体如下:

  • 如何通过调用多个微服务,来抽象出 Producer(生产者)的信息。

  • 在不同的渠道上(如电脑桌面、移动设备和平板电脑),应用程序需要不同的数据来响应相同的后端服务,比如:UI(用户界面)就可能会有所不同。

  • 不同的 Consumer(消费者)可能需要来自可重用式微服务的不同响应格式。谁将去做数据转换或现场操作?

  • 如何处理不同类型的协议?特别是一些可能不被 Producer 微服务所支持的协议。


解决方案:API 网关将有助于解决在微服务实施过程中所涉及到的上述关注点。


具体如下:

  • API 网关是任何微服务调用的统一入口。

  • 它像代理服务一样,能够将一个微服务请求路由到其相关的微服务处,并抽象出 Producer 的细节。

  • 它既能将一个请求扇出(fan out,输出)到多个服务上,也能汇总多个结果,并发回给 Consumer。

  • 鉴于通用 API 无法解决 Consumer 的所有请求,该方案能够为每一种特定类型的客户端创建细粒度的 API。

  • 它也可以将某种协议请求(如:AMQP)转换为另一种协议(如:HTTP),反之亦然,从而方便了 Producer 和 Consumer 的处理。

  • 它也可以将认证与授权存储库从微服务中卸载出去。


聚合器模式


问题:虽然我们已经在 API 网关模式中讨论了如何解决聚合数据的问题,不过我们仍将做进一步的讨论。


当我们将业务功能分解成多个较小的逻辑代码块时,有必要思考每个服务的返回数据是如何进行协作的。


显然,该责任不会留给 Consumer,那么我们就需要理解 Producer 应用的内部实现。


解决方案:聚合器模式将有助于解决该问题。它涉及到如何聚合来自不同服务的数据,然后向 Consumer 发送最终响应。


具体说来,我们有如下两种实现方法:

  • 复合微服务(Composite Microservice)将会去调用全部所需的微服务,整合各种数据,并在回传之前转换数据。

  • API 网关(API Gateway)也能对多个微服务的请求进行 Partition(分区),并在发送给 Consumer 之前聚合数据。


我们建议:如果您用到了任何业务逻辑的话,请选用复合微服务;否则请采用 API 网关方案。


客户端 UI 合成模式


问题:当各种服务按照业务功能和子域被分解开发时,它们需要根据用户体验的预期效果,从一些不同的微服务中提取数据。


在过去的单体应用中,我们只要从 UI 到后端服务的唯一调用中获取所有的数据,并刷新和提交到 UI 页面上便可。如今,情况则不同了。


解决方案:对于微服务来说,UI 必须被设计成单屏、单页面的多段、多区域的结构。


每一段都会去调用单独的后端微服务,以提取数据。像 Angular JS 和 React JS 之类的框架都能够实现为特定的服务合成 UI 组件。


通过被称为单页应用(Single Page Applications,SPA)的方式,它们能够使得应用程序仅刷新屏幕的特定区域,而不是整个页面。


数据库模式


按服务分配数据库


问题:您可能会碰到如何定义数据库架构的微服务问题。


下面是具体的关注点:

  • 服务必须是松散耦合的,以便能够被二次开发、部署和独立扩容。

  • 各个业务交易需要在横跨多个服务时,仍保持不变。

  • 某些业务交易需要从多个服务中查询到数据。

  • 数据库有时需要根据规模需求被复制与分片。

  • 不同的服务具有不同的数据存储需求。


解决方案:为了解决上述需求,我们需要通过设计为每个微服务配备一个独享的数据库模式。


即:该数据库仅能被其对应微服务的 API 单独访问,而不能被其他服务直接访问到。


例如,对于关系型数据库,我们可以使用:按服务分配私有表集(private-tables-per-service)、按服务分配表结构(schema-per-service)、或按服务分配数据库服务器(database-server-per-service)。


每个微服务应该拥有一个单独的数据库 ID,以便它们在独享访问的同时,禁止再访问其他的服务表集。


按服务共享数据库


问题:上面讨论的按服务分配数据库是一种理想的微服务模式,它一般被前面提到的 Greenfield 应用和 DDD 式的开发。但是,如果我们面对的是需要采用微服务的单体应用就没那么容易了。


解决方案:按服务共享数据库的模式虽然有些违背微服务的理念,但是它对于将前面提到的 Brownfield 应用(非新建应用)分解成较小的逻辑块是比较适用的。


在该模式下,一个数据库可以匹配不止一个的微服务,当然也至多 2~3 个,否则会影响到扩容、自治性和独立性。


命令查询职责隔离(CQRS)


问题:对于按服务分配数据库的模式而言,我们如何在微服务的架构中,实现对多个服务进行联合查询数据的需求呢?


解决方案:CQRS 建议将应用程序拆分成两个部分:命令和查询。命令部分主要处理创建、更新和删除之类的请求;查询部分则利用物化视图(Materialized Views)来处理各种查询。


它通常配合事件溯源模式(Event Sourcing Pattern)一起创建针对任何数据的变更事件。而物化视图则通过订阅事件流,来保持更新。


Saga 模式


问题:当每个服务都有自己的数据库,而且业务交易横跨多个服务时,我们该如何确保整体业务数据的一致性呢?


例如:对于某个带有客户信用额度标识的电商应用而言,它需要确保新的订单不会超出客户的信用额度。


但是,由于订单和客户分属不同的数据库,应用程序无法简单地实现本地交易的 ACID(原子性、一致性、隔离性、持久性)特性。


解决方案:Saga 代表了一个高层次的业务流程,它是由一个服务中的多个子请求,并伴随着逐个更新的数据所组成。在某个请求失败时,它的补偿请求会被执行。


实现方式有如下两种:

  • 编排(Choreography):没有中央协调器,每个服务都会产生并侦听其他服务的事件,以决定是否应采取行动。

  • 协调(Orchestrator):由一个中央协调器(对象)负责集中处理某个事件(Saga)的决策,和业务逻辑的排序。

观测模式


日志聚合


问题:我们来考虑这样一个用例:某个应用程序包括了那些在多台机器上运行的多个服务实例,各种请求横跨在这些多个服务实例之中。同时,每个服务实例都会生成一种标准格式的日志文件。


那么我们如何针对某个特定的请求,通过各种日志来理解该应用程序的行为呢?


解决方案:显然,我们需要一个集中化的日志服务,将各个服务实例的日志予以聚合,以便用户对日志进行搜索和分析。他们可以针对日志中可能出现的某些消息,配置相应的警告。


例如:PCF(Pivotal Cloud Foundry)平台拥有一个日志聚合器,它从每种元素(如:路由器、控制器等)中收集与应用相关的日志。而 AWS Cloud Watch 也具有相似的功能。


性能指标


问题:当各种服务组合随着微服务架构变得越来越复杂时,监控交易的完整性,并能够在出现问题时及时发出警告,就显得尤为重要了。那么我们该如何收集与应用相关的性能指标呢?


解决方案:为了收集不同操作的统计信息,并提供相应的报告和警告。


我们一般会用两种模式来聚集各项指标:

  • 推式:将各项指标推给专门的指标服务,如:NewRelic 和 AppDynamics。

  • 拉式:从指标服务处拉取各项指标,如:Prometheus。


分布式跟踪


问题:在微服务架构中,横跨多个服务的请求是比较常见的。某个服务需要通过横跨多个服务去执行一到多项操作,才能处理一些特定的请求。


那么,我们该如何通过跟踪某个端到端的请求,以获知出现的问题呢?


解决方案:我们需要一种具有特性的服务。


具体特性服务如下:

  • 为每个外部请求分配一个唯一的 ID。

  • 将该外部请求 ID 传给所有的服务。

  • 在所有的日志消息中都包含该外部请求 ID。

  • 在集中式服务中,记录处理外部请求的相关信息,包括:开始时间、结束时间、和执行时间。


Spring Cloud Slueth + Zipkin Server,是一种常见的实现方式。


健康检查


问题:我们在实施微服务架构的过程中,可能会碰到某个服务虽已启动,但是无法处理交易的情况。


那么,我们该如何通过负载均衡的模式,来确保请求不会“落入”失败的实例中呢?


解决方案:每个服务都需要有一个端点,通过诸如 /health 的参数,对应用进行健康检查。


该 API 需要能够检查主机的状态,其他服务与基础设施的连接性,以及任何特定的逻辑关系。


Spring Boot Actuator 不但能够实现端点的健康检查,还能够被定制实施。


横切关注点模式(Cross-Cutting Concern Patterns)


外部配置


问题:通常情况下,一个服务需要去调用其他的服务和数据库。在诸如开发、QA(Quality Assurance,质量保证)、UAT(User Acceptance Test,用户验收测试)、和生产环境中,端点的 URL、或某些配置的属性会有所不同。


因此,有时候我们需要对这些服务的各种属性进行重构、和重新部署。那么我们如何避免在配置变更中修改代码呢?


解决方案:外部化(externalize)所有的配置,包括各个端点的 URL 和信任凭据,以保证应用程序在启动时、或运行中能够加载它们。


Spring Cloud 配置服务器提供了向 GitHub 进行属性外部化的选项,并将其作为环境属性予以加载。


此法保证了应用程序能够在启动时就被访问到,或是在不重启服务器的情况下实现刷新。


服务发现模式


问题:当微服务初具规模时,我们需要考虑如下两个关于调用服务方面的问题。


具体问题如下:

  • 由于采用了容器技术,IP 地址往往被动态地分配给不同的服务实例。因此,每次当 IP 地址发生变化时,Consumer 服务可能会受到影响,需要我们手动更改。

  •  Consumer 需要记住每个服务的 URL,这就倒退成了紧耦合的状态。


那么,Consumer 或路由器该如何获知所有可用的服务实例与位置呢?


解决方案:我们需要创建一个服务注册表,来保存每个 Producer 服务的元数据(Meta Data)。


一个服务实例在启动时,应当被注册到表中;而在关闭时,需从表中被注销。


Consumer 或路由器通过查询该注册表,就能够找到服务的位置。Producer 服务也需要对该注册表进行健康检查,以确保能够消费到那些可用的、且正在运行的服务实例。


我们一般有两种服务发现的类型:客户端和服务器端。使用客户端发现的例子是 Netflix Eureka;而使用服务器端发现的例子是 AWS ALB。


断路器模式


问题:有时候,某个服务在调用其他服务,以获取数据的时候,会出现下游服务(Downstream Service)“掉线”的情况。


它一般会带来两种结果:

  • 该请求持续发往该掉线服务,直至网络资源耗尽和性能降低。

  • 用户产生不可预料的、较差的使用体验。


那么我们该如何避免服务的连锁故障,并妥善处置呢?


解决方案:Consumer 应该通过一个代理来调用某项远程服务,就像电路中的断路器一样。


当出现持续失败的数量超过设定阈值时,断路器就会“跳闸”一段时间,从而导致所有调用远程服务的尝试被立即切断。


在超过设定时间之后,断路器只允许有限数量的测试请求通过。而如果这些请求成功了,那么断路器将恢复正常运行;否则判定为故障依旧,并重新开始新的定时周期。


Netflix Hystrix 就很好地使用了该断路器模式。它可以在断路器“跳闸”的时候,帮助您定义一种回退机制,以提供更好的用户体验。


蓝绿部署模式


问题:在微服务架构中,一个应用程序可以有多个微服务。如果我们为了部署一个增强版,而停止所有的服务,那么停机时间一旦过长,就会对业务造成影响。


况且,这对于回退来说也将会是一场噩梦。那么我们该如何避免、或减少部署过程中服务的停机时间呢?


解决方案:我们可以采用蓝绿部署的策略,以减少或消除停机时间。在蓝、绿两个相同的生产环境中,我们假设绿色环境有着当前真实的实例,而蓝色环境具有应用程序的最新版本。


在任何时候,只有一个环境能够处理所有真实的流量,并对外提供服务。如今,所有的云服务平台都能提供基于蓝绿部署的选项。


当然,我们还可以采用许多其他的微服务架构模式,如:Sidecar 模式、链式微服务(Chained Microservice)、分支微服务(Branch Microservice)、事件溯源模式(Event Sourcing Pattern)、和持续交付方式等。


本文作者:Rajesh Bhojwani,陈峻编译

本文首发于联盟技术公众号“51CTO技术栈”。

本公众号编辑部维护读者群之架构群,邀请了坐馆老司机李艳鹏、曲健、伟山、史海峰嘉宾等参与交流。加群请在公众号回复:架构群。


往期推荐:


技术琐话 



以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存